#!/usr/bin/env python3 
# 
# Node/javascript mavlink test maker!
#
# This is written to run with python3 only, and the tests it produces are for node / javascript
# It is mavlink1 and mavlink2 and multi-type aware
#
# Copyright David 'Buzz' Bussenschutt July 2020
# Released under GNU GPL version 3 or later
#
#  to run:  cd test
#            python3 make_tests.py > made_tests.js
#            cd - 
#           mocha test --grep 'from C'
#
# reads the output from modified C test commands, for example, for ardupilot + mavlink2:
# "./testmav2.0_ardupilotmega | grep '^fd' command and builds a bunch of .js tests for this test data
#
# In order to identify types that are 'Long' or need Array wrapping, which need special handling we build a little lookup table 
# from the autogenerated file:mavlink.tests.js" (which itself is made by mavgen_javascript.py )
#
# we label and number each of the tests in the output as well, so that its also easy to run individual tests with something like:
# mocha test --grep '1234'
# or we can run all of them with:
# mocha test --grep 'from C'
# or eg all the ardupilotmega tests against mavlink1:
# mocha test --grep 'using ardupilotmega/1.0'

import subprocess
import sys

# now tested and executes with this simple matrix of two mavtypes and two mav versions
cmddir = '../../../generator/C/test/posix/'
mavtypes = ['ardupilotmega','common']
versions = ['1.0','2.0']
cmds = []

#..so the C binding cmds executed/wrapped are: 'testmav1.0_ardupilotmega', 'testmav2.0_ardupilotmega', 'testmav1.0_common', 'testmav2.0_common'

#---------------------------------------------------------------------------------------


template1 = '''
  it('id${ID} encode and decode ${NAME} from C using ${MAVTYPE}/${VERSION} ${SIGNED}', function() {
        
        this.mav.seq = ${SEQ};
        this.mav.srcSystem=${SRCSYS};
        this.mav.srcComponent=${SRCCOMP};
'''

signing_extra_template = '''

        //-------- START codeblock only for signed packets----------------

        this.mav.seq = ${SEQ}-1;

        // relevant to how we pass-in the Long object/s to jspack, we'll assume the calling user is smart enough to know that. 
        var wrap_long = function (someLong) { 
            return [someLong.getLowBitsUnsigned(), someLong.getHighBitsUnsigned()]; 
        } 

        this.mav.signing.secret_key = new Buffer.from([ 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42 ]) ; // matches secret key in testmav.c
        this.mav.signing.link_id = 0 ;    // 1 byte // matches link_id in testmav.c
        //this.mav.signing.timestamp = new Buffer.from([ 0,0,0,0,0,${TS}]); // 6 bytes // matches timestamp in testmav.c
        this.mav.signing.timestamp = ${TS};  // at most 48 bits , fits in a native js number  - matches timestamp in testmav.c
        this.mav.signing.sign_outgoing = true; // todo false this

        var epoch_offset = 1420070400; 
        var x= Long.fromString("${TS}", true); 
        var long_timestamp = wrap_long(x); 

        var target_system = 255;  // the python impl in mavproxy uses 255 here , so we do, it could be this.sysid 
        var target_component = 0; 
        var secret_key = this.mav.signing.secret_key ; 

                    MAVLink20Processor.prototype.send = function(mavmsg) {
                        buf = mavmsg.pack(this); 
                        // no actual send here
                        this.seq = (this.seq + 1) % 256; 
                        this.total_packets_sent +=1; 
                        this.total_bytes_sent += buf.length; 
                    } 

        var link_id =0; 
        var srcSystem=this.mav.srcSystem; 
        var srcComponent=this.mav.srcComponent; 
        stream_key = new Array(link_id,srcSystem,srcComponent).toString(); 
        this.mav.signing.stream_timestamps[stream_key] = ${TS}; 
        this.mav.signing.timestamp.should.eql(${TS}); //ts before setup

        var setup_signing = new mavlink20.messages.setup_signing(target_system, target_component, secret_key, long_timestamp); 
        this.mav.send(setup_signing,this.sysid); 

        setup_signing.secret_key.should.eql(new Buffer.from([ 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42 ]) ); 
        setup_signing.initial_timestamp.should.eql([${TS},0]); 
        //this.mav.signing.timestamp.should.eql(new Buffer.from([0,0,0,0,0,${TS}])); 
        this.mav.signing.timestamp.should.eql(${TS}+1); // ts after setup 
        this.mav.signing.link_id.should.eql(0); 
        this.mav.signing.sign_outgoing.should.eql(true);

        //-------- END codeblock only for signed packets----------------
'''

template2 = '''

        var test_${NAME} = this.tests.test_${NAME}()[0]; // get the assembled test object with test data already set, override just the min we need to do this test

//---  you can uncomment any of these to change the test, but you'll need to change the reference buffer to the right result too       
//${FIELDS}
//--- 

        // Create a buffer that matches what the Python version of MAVLink creates
        var reference = new Buffer.from([${BUFFER}]);

        this.mav.signing.timestamp = ${TS};// force ts to be correct, right before the pack() that matters

        var p = test_${NAME}.pack(this.mav);

 //       console.log(p); 
 //       p.forEach( x => {  process.stdout.write( x.toString() ); process.stdout.write(" ") } ); process.stdout.write("\\n"); 
 //       p.forEach( x => {  process.stdout.write( x.toString(16) ); process.stdout.write(" ") } ); process.stdout.write("\\n"); 

        test_${NAME}._header.seq.should.eql(${SEQ}); 
        test_${NAME}._header.srcSystem.should.eql(${SRCSYS}); 
        test_${NAME}._header.srcComponent.should.eql(${SRCCOMP}); 
        test_${NAME}._header.msgId.should.eql(test_${NAME}._id); 

        ${SIGNED}test_${NAME}._header.incompat_flags.should.eql(1); 
        ${UNSIGNED}test_${NAME}._header.incompat_flags.should.eql(0); 
        
        test_${NAME}._header.compat_flags.should.eql(0); 

        new Buffer.from(p).should.eql(reference);
    });
'''

templatestart = '''
//
// (auto-generated by make_tests.py ), do not edit.
// generator by davidbuzz@gmail.com
//
// Copyright David 'Buzz' Bussenschutt July 2020
// Released under GNU GPL version 3 or later
//
// you can regenerate this file and its dependencies and run its tests, by executing the following:  "cd pymavlink/generator/javascript ; npm test"
// or see make_tests.py which created this.
//
should = require('should');
var Long = require('long');

'''

templateheader = '''
//--------------------------------------------------------------------------------------------------------------------------------------------------------

describe('end-to-end node byte-level tests of ${MAVTYPE}/${VERSION} against C impl', function() {

    beforeEach(function() {

        var {mavlink${VERS}, MAVLink${VERS}Processor} = require('../implementations/mavlink_${MAVTYPE}_v${VERSION}/mavlink.js');// hardcoded here by make_tests.py generator
        this.mav = new MAVLink${VERS}Processor(null, 42, 150);  // hardcoded here by make_tests.py generator
        this.tests = require('../implementations/mavlink_${MAVTYPE}_v${VERSION}/mavlink.tests.js');//// hardcoded here by make_tests.py generator
        // be sure the test library is using the right version before we call into it
        this.tests.set_mav(this.mav);
    });'''

templatefooter = '''

});'''

#------------------------------------------------

def is_packet_and_field_in_long_list(pname,fname):
    for l in llines:
        if ( pname+'.'+fname in l ) : 
            return (True, l)
    return (False, '')

testid = 1;

# for each of the 1.0,2.0 and common,ardupilotmega combos write tests
def do_make_output(mt,v,lines):
  global testid
  t = templateheader.replace('${MAVTYPE}',mt)
  t =              t.replace('${VERSION}',v)
  t =              t.replace('${VERS}',v.replace('.',''))
  print(t)

  last_line = ''
  for line in lines:
    if line.startswith('fd '): # mavlink2 start byte as human-readable hex, eg 'fd 08 7e 2a 0b e2 00 00 88 41 00 00 34 42 30 93 '
        last_line = '2.0'
        if v == 1: # if param 'v' requested mav1 data and we see mav2 data, ignore it
            continue
        hexdata = line.strip().replace(' ',', 0x')
        hexdata = '0x'+hexdata
        # ckeck if signing bit is set on this packet: 0xfd, 0x09, 0x01 <-- that 1
        signchar = hexdata[15]
        signint = int(signchar,16)
        signbit = signint % 2; # lowest bit to true/false

    if line.startswith('fe '): # mavlink1 start byte as human-readable hex, eg 'fe 08 7e 2a 0b e2 00 00 88 41 00 00 34 42 30 93 '
        last_line = '1.0'
        if v == 2: # if param 'v' requested mav2 data and we see mav1 data, ignore it
            continue
        hexdata = line.strip().replace(' ',', 0x')
        hexdata = '0x'+hexdata
        signbit = False

    if line.startswith('sysid:'): # packet details human-readable eg 'sysid:42 compid:11 seq:126 RPM { rpm1: 17.000000  rpm2: 45.000000  }'
        # skip parsing data if it's not this parser
        if last_line != v:
            continue
        fields = line.split(' ') 
        sysid = fields[0].split(':')[1]
        compid = fields[1].split(':')[1]
        seq = fields[2].split(':')[1]

        # lines without sign_ts, the { is earlier
        if fields[4] == '{':
            sign_ts = '0'
            packetname = fields[3]
            more = fields[4:]
        else:
            sign_ts = fields[3].split(':')[1]
            packetname = fields[4]
            more = fields[5:]

        packetname = packetname.lower()

        arraystarted = False
        for i,x in enumerate(more):
            
            if x == '[':
                arraystarted = True
            if x == ']':
                arraystarted = False
                more[i] = '], \n            '
            if not arraystarted and x == '':
                more[i] = ',\n            '
        fixed = ''.join(more)
        fixed = fixed.replace(',]',']') # drop unneeded comma from end of arrays
        fixed = fixed.replace('{','');
        fixed = fixed.replace('}','');
        fixed = fixed.replace(':','=')   # move : to =
        import re
        fixed = re.sub(r'(\'.*?\')', '\\1,\n            ', fixed)

        # now iterate over them as parsed lines safely
        newlines = []
        for fieldline in fixed.split('\n'):
            if fieldline.strip() == '':
                continue
            # a little miniparser here to modify things after the = sign to insert our own test values, not the included ones, but leave
            # value wrapping and other surrounding casting functions as-is.
            ( field, value) = fieldline.split('='); 
            if not value.startswith('['):
                value = value.replace(',','')
                field = field.replace(' ','')
            else: # array
                value = value.split('[')[1].split(']')[0]; # stuff inside the brackets, but not the brackets
            (retval,match) = is_packet_and_field_in_long_list(packetname,field);
            if retval == True:                
                parts = match.split('=');
                after_equals = parts[1];
                before_semicolon = after_equals.split(';')[0]

                # determine old test value in the 'before_semicolon' line segment:
                # a little miniparser here to modify things after the = sign to insert our own test values, not the included ones, but leave
                # value wrapping and other surrounding casting functions as-is.
                if not before_semicolon.replace(' ','').startswith('['):
                    if '"' in before_semicolon:
                        oldvalue = before_semicolon.split('"')[1]; # stuff inside the ", but not the " 
                    elif 'Array' in before_semicolon:
                        oldvalue = before_semicolon.split('[')[1].split(']')[0]; # [1234]
                    else:   
                        oldvalue = before_semicolon; # unwrapped numbers etc
                else: # array
                    oldvalue = before_semicolon.split('[')[1].split(']')[0]; # stuff inside the brackets, but not the brackets                

                line_minus_data = before_semicolon.replace(oldvalue,value);
                newlines.append("      test_"+packetname+"."+field+'='+line_minus_data+';')
            else:
                newlines.append("      test_"+packetname+"."+field+'='+value+';')


        fixed = '\n//'.join(newlines)

        t = template1
        t = t.replace('${MAVTYPE}',mt)
        t = t.replace('${VERSION}',v)
        t = t.replace('${NAME}',packetname)
        t = t.replace('${ID}',str(testid))
        testid = testid+1
        t = t.replace('${SEQ}',seq)
        t = t.replace('${SRCSYS}',sysid)
        t = t.replace('${SRCCOMP}',compid)
        t = t.replace('${BUFFER}',hexdata)
        if signbit:
            t = t.replace('${SIGNED}','signed')
        else :
            t = t.replace('${SIGNED}','')
        print(t)

        if signbit:
            t = signing_extra_template
            t = t.replace('${SEQ}',seq)
            t = t.replace('${TS}',sign_ts)
            print(t)

        t = template2
        t = t.replace('${NAME}',packetname)
        t = t.replace('${SEQ}',seq)
        t = t.replace('${SRCSYS}',sysid)
        t = t.replace('${SRCCOMP}',compid)
        t = t.replace('${BUFFER}',hexdata)
        t = t.replace('${FIELDS}',fixed) 
        t = t.replace('${TS}',sign_ts)
        if signbit:
            t = t.replace('${SIGNED}','/*signed*/ ')
            t = t.replace('${UNSIGNED}','//unsigned ')
        else :
            t = t.replace('${SIGNED}','//signed ')
            t = t.replace('${UNSIGNED}','/*unsigned*/ ')
        print(t)

        print('//------------------------------------------------------')

  # append footer
  t = templatefooter.replace('${MAVTYPE}',mt)
  t =              t.replace('${VERSION}',v)
  t =              t.replace('${VERS}',v.replace('.',''))
  print(t)


#---------------------------------------------------------------------------------------

# example line from file:
#       test_actuator_output_status.time_usec =  Long.fromNumber(93372036854775807, true); // fieldtype: uint64_t  isarray: False 
llines = []
def make_long_lookup_table(mt,v):
    global llines
    _cmd = 'egrep "(wrap_long|new.*Array)" ../implementations/mavlink_'+mt+'_v'+v+'/mavlink.tests.js'; # relevant lines only
    _result = subprocess.run(_cmd, stdout=subprocess.PIPE, shell=True)
    _data = _result.stdout.decode('utf-8')
    llines = _data.split("\n")
    #return lines

#---------------------------------------------------------------------------------------

print(templatestart)

for mt in mavtypes:
    for v in versions:
        cmd = cmddir+'testmav'+v+'_'+mt
        result = subprocess.run(cmd, stdout=subprocess.PIPE)
        data = result.stdout.decode('utf-8')
        lines = data.split("\n")
        llines = []
        make_long_lookup_table(mt, v);
        do_make_output(mt,v,lines)


print("//output done")

sys.exit()

